<?php
namespace core\models;
/**
 * 数据库操作抽象类
 * @package 数据库
 * @author  ZhouHaoHua <53027327@qq.com>
 */
abstract class Abstract_Database {
    /**
     * 数据库资源连接
     * @var mysqli
     */
    protected $link;
    /**
     * 数据库字符集
     * @var string
     */
    protected $charset;

    /**
     * 初始化
     * @param string $server   数据库服务器地址
     * @param string $username 用户名
     * @param string $password 密码
     * @param string $database 数据库名称
     * @param string $charset  数据字符集，如：utf8, latin1等
     */
    public function __construct($server, $username, $password, $database, $charset) {
        $this->charset = $charset;
        $this->connect($server, $username, $password, $database);
    }

    /**
     * 数据库连接
     * @param string $server   数据库服务器地址
     * @param string $username 用户名
     * @param string $password 密码
     * @param string $database 数据库名称
     * @return Ambigous <NULL, resource> 返回空或数据资源
     */
    abstract protected function connect($server, $username, $password, $database);

    /**
     * 关闭数据库连接句柄
     * @internal param string $link 句柄名称
     * @return bool
     */
    abstract public function close();

    /**
     * 错误信息提示
     * @param string $query SQL查询语句
     * @param string $errno 错误代码号
     * @param string $error 错误信息字符串
     * @throws \ErrorException
     */
    final protected function error($query, $errno, $error) {
        $err = ('<font color="#000000"><strong>' . $errno . ' - ' . $this->getErrorMsg($errno).$error . '<br /><br />' . $query . '<br /><br /><small><font color="#ff0000">[TEP STOP]</font></small><br /><br /></strong></font>');
        throw \Z::Error('SQL执行出错！' . $err);
    }

    /**
     * 根据错误代码返回错误文字
     * @param $errno
     * @return string
     * @author ZhouHaoHua <53027327@qq.com>
     */
    private function getErrorMsg($errno){
        $err['1054'] = '不存在的字段:';
        if($err[$errno]){
            return $err[$errno];
        }
        return '';
    }

    /**
     * 数据库query查询
     * @param string $query SQL查询语句
     * @return resource 返回一个资源标识符。仅对 SELECT，SHOW，EXPLAIN 或 DESCRIBE 语句返回一个资源标识符，如果查询执行不正确则返回 FALSE。对于其它类型的 SQL 语句，mysql_query() 在执行成功时返回 TRUE，出错时返回 FALSE。非 FALSE 的返回值意味着查询是合法的并能够被服务器执行。
     */
    abstract public function query($query);

    /**
     * 更新或插入资料到数据库
     * @param string $table      插入的表
     * @param array  $data       插入的数据,传入的是数组,key保存字段名称,value保存字段的值
     * @param string $action     执行的操作 支持：insert,update,replaceinto
     * @param string $parameters 查询条件
     * @return resource
     */
    public function perform($table, $data, $action = 'insert', $parameters = '') {
        reset($data);  //将数组的内部指针指向第一个单元
        if ($action == 'insert' || $action == 'replaceinto') {
            $query = 'insert ';
            if ($action == 'replaceinto') {
                $query = 'replace ';
            }
            $query .= ' into ' . $table . ' ('; //字符串的拼接
            foreach($data as $columns => $v) {
                $query .= $columns . ', '; //字符串的拼接
            }
            $query = substr($query, 0, -2) . ') values (';  //字符串的拼接
            reset($data);   //将数组的内部指针指向第一个单元
            foreach($data as $key => $value) {
                //(string)为强制转换成字符串

                switch ((string) $value) {
                    case 'now()':
                        $query .= 'now(), ';  //字符串的拼接
                        break;
                    case 'null':
                    case null:
                        $query .= 'null, ';  //字符串的拼接
                        break;
                    default:
                        $query .= '\'' . $this->input($value) . '\', ';  //使用函数sql_input
                        break;
                }
            }
            $query = substr($query, 0, -2) . ')';                //字符串的拼接
        } elseif ($action == 'update') {
            $query = 'update ';
            $query .= $table . ' set ';            //字符串的拼接

            foreach($data as $columns => $value) {
                switch ((string) $value) {
                    case 'now()':
                        $query .= $columns . ' = now(), ';            //字符串的拼接
                        break;
                    case 'null':
                    case null:
                        $query .= $columns . ' = null, ';            //字符串的拼接
                        break;
                    default:
                        $query .= $columns . ' = \'' . $this->input($value) . '\', ';    //字符串的拼接
                        break;
                }
            }
            //substr()为截取字符串函数，以下表示的是从头到倒数第二个字符结束的字符串
            $query = substr($query, 0, -2) . ' where ' . $parameters;        //字符串的拼接
        }
        return $this->query($query);         //返回执行该字符串
    }

    /**
     * 从结果集中取得一行作为关联数组
     * @param mysqli_result $db_query 由mysqli_query(), mysqli_store_result() 或 mysqli_use_result()查询返回的资源标识
     * @return array
     */
    abstract protected function fetch_array($db_query);

    /**
     * 返回查询的记录总数
     * @param mysqli_result $db_query SQL query 查询返回的资源标识
     * @return int
     */
    abstract public function num_rows($db_query);

    /**
     * 将指针指向结果集($db_query)的某一行($rows_number)
     * @param mysqli_result $db_query   SQL query 查询返回的资源标识
     * @param int           $row_number 结果集的行号
     * @return bool
     */
    abstract public function data_seek($db_query, $row_number);

    /**
     * 获得当前插入记录的ID
     * @return number
     */
    abstract public function insert_id();

    /**
     * 仅需要在考虑到返回很大的结果集时会占用多少内存时调用。在脚本结束后所有关联的内存都会被自动释放。
     * @param mysqli_result $db_query SQL query 查询返回的资源标识
     * @return boolean
     */
    abstract public function free_result($db_query);

    /**
     * 从结果集中取得列信息并作为对象返回
     * @param mysqli_result $db_query SQL query 查询返回的资源标识
     * @return object
     */
    abstract public function fetch_fields($db_query);

    /**
     * 输出过滤。把html代码和js代码转成安全的字符串，一般在数据输出页面之前使用
     * @param string|array $string   字符串或数组
     * @param string       $char_set 【源】字符编码默认gb2312
     * @return string|array
     */
    public function output($string, $char_set = 'gb2312') {
        if (is_string($string)) {
            return Strings::SafeOut($string, ENT_COMPAT | ENT_HTML401, $char_set);
        } elseif (is_array($string)) {
            reset($string);
            foreach($string as $key => $value) {
                $string[$key] = $this->output($value, $char_set);
            }
            return $string;
        } else {
            return $string;
        }
    }

    /**
     * 转义 SQL 语句中使用的字符串中的特殊字符。下列字符受影响：\x00    \n    \r    \    '    "    \x1a
     * @param string $string 源字符串
     * @return string|unknown
     */
    abstract public function input($string);

    /**
     * 为数据输入数据库做准备
     * 当传入参数为字符串或者值为字符串的数组时,去除 字符串 或 数组中的值 的首尾空格,然后将字符串中的多个空格用一个空格替代, '<' 或者 '>' 符号用 '_' 替代
     * 当传入非字符串或者值不是字符串的数组时, 不做处理。注意：后台的处理方式与这里不一样，后台写了一个单独的方法！
     * @param string $string 源字符串
     * @return array|string
     */
    public function prepare_input($string) {
        if (is_string($string)) {
            return trim(Strings::stripslashes2($string));
        } elseif (is_array($string)) {
            reset($string);
            foreach($string as $key => $value) {
                $string[$key] = $this->prepare_input($value);
            }
            return $string;
        } else {
            return $string;
        }
    }

    /**
     * 取得指定表的字段名称
     * @author howard
     * @param  string $table_name 数据表名称
     * @return array
     */
    public function table_fields_names($table_name) {
        $meta  = false;
        $array = $this->table_fields($table_name);
        foreach ($array as $key => $val) {
            $meta[] = $val["Field"];
        }
        return $meta;
    }

    /**
     * 取得指定表的字段信息数组
     * @author howard
     * @param  string $table_name
     * @return array
     */
    public function table_fields($table_name) {
        $data   = array();
        $result = $this->query("describe $table_name ");
        while ($d_row = $this->fetch_array($result)) {
            $data[] = $d_row;
        }
        return $data;
    }

    /**
     * 取得某表的主键名称
     * @param string $table_name
     * @return array
     */
    public function table_primary_keys($table_name) {
        $data   = array();
        $fields = $this->table_fields($table_name);
        foreach ($fields as $field) {
            if ($field['Key'] == 'PRI')
                $data[] = $field['Field'];
        }
        return $data;
    }

    /**
     * 快速插入数据到指定数据表
     * @param string $table         表名
     * @param array  $formFields    字段值数组（原始的表单数据，不用经过$this->prepare_input()处理）
     * @param string $disableFields 被排除的字段，即数据库表哪些栏位是不能改动的，多个栏位用“,”分隔
     * @return int
     */
    public function fast_insert($table, $formFields, $disableFields = "") {
        if (!empty($disableFields)) {
            $disableFields = explode(',', preg_replace('/[[:space:]]+/', '', $disableFields));
        }
        $insert_id      = 0;
        $fields         = $this->table_fields_names($table);
        $formFields     = $this->prepare_input($formFields);
        $sql_data_array = false;
        if (sizeof($fields)) {
            foreach ($fields as $key => $val) {
                if (array_key_exists($val, $formFields) && !is_array($formFields[$val])) {
                    if (!in_array($val, (array) $disableFields) || $disableFields == "") {
                        $sql_data_array[$val] = $formFields[$val];
                    }
                }
            }

            if (is_array($sql_data_array)) {
                $this->perform($table, $sql_data_array);
                $insert_id = $this->insert_id();
            }
        }

        return $insert_id;
    }

    /**
     * 快速更新表数据
     * @param string $table       表名
     * @param string $where       条件字符
     * @param array  $formFields  字段值数组（原始的表单数据，不用经过$this->prepare_input()处理）
     * @param string $allowFields 允许更新的字段，如果值为*则可以更新所有字段，多栏位用“,”分隔
     * @return int
     */
    public function fast_update($table, $where, $formFields, $allowFields = "*") {

        if ($allowFields != "*") {
            $allowFields = explode(',', preg_replace('/[[:space:]]+/', '', $allowFields));
        }
        $fields         = $this->table_fields_names($table);
        $formFields     = $this->prepare_input($formFields);
        $sql_data_array = false;

        if (sizeof($fields)) {
            foreach ($fields as $key => $val) {
                if (array_key_exists($val, $formFields) && !is_array($formFields[$val])) {
                    if (in_array($val, (array) $allowFields) || $allowFields == "*") {
                        $sql_data_array[$val] = $formFields[$val];
                    }
                }
            }
            if (is_array($sql_data_array)) {

                $this->perform($table, $sql_data_array, 'update', $where);
                return (int) $this->affected_rows();
            }
        }
        return 0;
    }

    /**
     * 取得前一次 MySQL 操作所影响的记录行数
     * @return integer
     */
    abstract public function affected_rows();

    /**
     * 判断数据表的某个字段是否存在
     * @param string $table 表名
     * @param string $field 字段名
     * @return bool 返回布尔值
     */
    public function field_exists($table, $field) {
        $v      = false;
        $result = $this->query("describe $table");
        while ($d_row = $this->fetch_array($result)) {
            if ($d_row["Field"] == "$field") {
                $v = true;
            }
        }
        return $v;
    }

    /**
     * 快速取得某表中某个字段的值，如快速取得产品ID为125的产品名称等
     * @param string $field_name 字段名称
     * @param string $table_name 数据表名称
     * @param string $where      查询条件(不含where字符)
     * @example $this->get_field_value('products_name', 'products_description', 'products_id=125 AND language_id=1 ')
     */
    final public function get_field_value($field_name, $table_name, $where = ' 1 ') {
        $field_name = trim($field_name);
        $sql        = $this->query('SELECT ' . (string) $field_name . ' FROM ' . (string) $table_name . ' WHERE ' . (string) $where . ' Limit 1 ');
        $row        = $this->fetch_array($sql);
        $name       = preg_replace('/(.+\.)/', '', $field_name);
        $name       = preg_replace('/.+ AS (\S+).*/i', '$1', $name);
        return $row[$name];
    }

    /**
     * 快速取得某个数据表的其中一行信息
     * @param string|array $table_name 表名
     * @param string|int   $where      条件 可写字符串或此表的主键id的值
     * @return array
     * @throws \ErrorException
     */
    public function get_one_row($table_name, $where = ' 1 ') {
        if (is_array($table_name) && is_numeric($where)) {
            throw \Z::Error(print_r($table_name, true) . '是数组，不能和数字的条件' . $where . '使用！');
        }
        //表
        if (is_array($table_name)) {
            $table_name = $this->table_array_to_string($table_name);
        }
        //条件
        if (is_numeric($where)) {
            $key_names = $this->table_primary_keys($table_name);
            if (sizeof($key_names) === 1) {
                $where = $key_names[0] . '=' . $where;
            } else {
                throw \Z::Error('数据表' . $table_name . '的主键必须是1个字段才能使用！');
            }
        } elseif (is_array($where)) {
            $where = $this->where_array_to_string($where);
        }

        $str = 'SELECT * FROM ' . (string) $table_name . ' WHERE ' . (string) $where . ' Limit 1 ';
        $sql = $this->query($str);
        return $this->fetch_array($sql);
    }

    /**
     * 取得一个数据表指定字段的记录
     * @param string|array $table_name 表名
     * @param string|array $field      字段字符串 field1,field2,....
     * @param string|int   $where      条件 可写字符串或此表的主键id的值
     * @throws \ErrorException
     * @return array
     */
    public function get_row($table_name, $field, $where = ' 1 ') {
        if (is_array($table_name) && is_numeric($where)) {
            throw \Z::Error(print_r($table_name, true) . '是数组，不能和数字的条件' . $where . '使用！');
        }
        //字段
        $field = $field ? $field : '*';
        //表
        if (is_array($table_name)) {
            $table_name = $this->table_array_to_string($table_name);
        }
        //条件
        if (is_numeric($where)) {
            $key_names = $this->table_primary_keys($table_name);
            if (sizeof($key_names) === 1) {
                $where = $key_names[0] . '=' . $where;
            } else {
                throw \Z::Error('数据表' . $table_name . '的主键必须是1个字段才能使用！');
            }
        } elseif (is_array($where)) {
            $where = $this->where_array_to_string($where);
        }

        $sql = $this->query('select ' . $field . ' from ' . $table_name . ' WHERE ' . (string) $where . ' Limit 1');
        return $this->fetch_array($sql);
    }

    /**
     * 删除数据记录
     * @param string $table 表名
     * @param string $where 删除条件
     * @return integer 返回被删除的记录数
     */
    public function delete($table, $where) {
        $this->query('delete from ' . $table . ' where ' . $where);
        return $this->affected_rows();
    }

    /**
     * SQL 查询语句格式化
     * @param string $tableStr  表名，如table_0 as t0, table_1 as t1
     * @param string $whereStr  条件 t0.id=t1.id
     * @param string $fieldsStr 字段名称，默认是*
     * @param string $groupBy   聚合内容，默认为空。如：t0.id
     * @param string $orderBy   排序方式，默认为空。如：t0.id ASC
     * @param string $Limit     取得几条。支持：0, 100或100的写法
     * @return string
     */
    public function formatSelectStr($tableStr, $whereStr, $fieldsStr, $groupBy, $orderBy, $Limit) {
        //$query = 'SELECT '.$fieldsStr.' FROM '.$tableStr.' WHERE '.$whereStr.' GROUP BY '.$groupBy.' ORDER BY '.$orderBy.' Limit '.$Limit;
        $query = '';
        $query .= 'SELECT ' . ($fieldsStr ? $fieldsStr : '*');
        $query .= ' FROM ' . $tableStr;
        $query .= ' WHERE ' . (trim($whereStr) ? $whereStr : ' 1 ');
        $query .= (trim($groupBy) ? ' GROUP BY ' . $groupBy : '');
        $query .= (trim($orderBy) ? ' ORDER BY ' . $orderBy : '');
        $query .= (trim($Limit) ? ' Limit ' . $Limit : '');
        //echo $query;exit;
        return $query;
    }

    abstract public function __destruct();

    /**
     * 判断某个数据表是否存在
     * @param $table_name 表名
     * @return bool true为存在
     */
    abstract public function table_exists($table_name);

    /**
     * 将数组格式的条件变成条件字符串
     * @param array  $where 条件数组
     * @param string $relationship_symbol 关系符号，目前只测了（and or)
     * @return string 返回条件字符串
     */
    public function where_array_to_string($where,$relationship_symbol = 'AND') {
        if (is_array($where)) {
            $array = array();
            foreach ($where as $k => $v) {
                if (stripos($k, ' >') !== false || stripos($k, ' <') !== false) {    //>= > < <= 操作
                    $array[] = $k . ' "' . $this->input($this->prepare_input($v)) . '" ';
                } elseif (strpos($k, '=') !== false) {   //如果是多表联合查询的关联条件在这里处理
                    $array[] = $k . ' ';
                } elseif (stripos($k, ' LIKE') > 0) {  //模糊搜索
                    $array[] = $k . ' "' . $this->input($this->prepare_input($v)) . '" ';
                } elseif (stripos($k, ' FIND_IN_SET') > 0) {    //FIND_IN_SET操作
                    $str     = ' FIND_IN_SET("' . $this->input($this->prepare_input($v)) . '", ' . substr($k, 0, (strlen(' FIND_IN_SET') * -1)) . ') ';
                    $array[] = $str;
                } elseif (stripos($k, ' IN') > 0) {    //IN操作以及NOT IN操作
                    $v = str_replace(['"',"'"],['',''],strip_tags($v));
                    $v = '"'.str_replace(',','","',$v).'"';
                    $array[] = $k . ' (' . $v . ') ';
                }elseif(stripos($k, 'IS NULL') !==false) {
                    $array[] = $k;
                }else {
                    $array[] = $k . '="' . $this->input($this->prepare_input($v)) . '" ';
                }
            }
            $where = implode(' ' . $relationship_symbol . ' ', $array);
        }
        return $where;
    }

    /**
     * 将数组格式的表变成表字符串
     * @param $tables
     * @return string 返回表字符串
     */
    public function table_array_to_string($tables = array()) {
        $_name = array();
        foreach ($tables as $k => $table) { //注意：$k是表$table的别名
            $_name[] = $table . ' ' . $k;
        }
        $name = implode(', ', $_name);
        return $name;
    }
}